package org.gfix.binding;

import java.lang.reflect.Field;
import java.util.EnumMap;
import java.util.Map;

import org.gfix.annotation.FixBinding;
import org.gfix.annotation.FixBindingSet;
import org.gfix.annotation.FixBound;
import org.gfix.annotation.FixGroup;
import org.gfix.annotation.FixGroupSet;
import org.gfix.annotation.FixMessageBinding;
import org.gfix.annotation.FixMessageBindingSet;
import org.gfix.converter.EnumConverter;
import org.gfix.parser.ValidatingValidator;
import org.gfix.types.FixVersion;

class ClassFixBindingDescription
{
	private Class<?> forType;
	private Map<FixVersion, FixBindingMetaData> versionMetadata 
		= new EnumMap<FixVersion, FixBindingMetaData>(FixVersion.class);
	
	private Map<FixVersion, ClassLevelBinding> classLevelTags = 
		new EnumMap<FixVersion, ClassLevelBinding>(FixVersion.class);
	
	public ClassFixBindingDescription(Class<?> forType)throws FixBindingException
	{
		this.forType = forType;
		parseClass();
		parseFields();
	}
	
	public Class<?> getBoundType()
	{
		return forType;
	}
	
	public Object createBoundObject()throws FixBindingException
	{
		try {
			return forType.newInstance();
		} catch (InstantiationException e) {
			throw new FixBindingException("Cannot create bound bean",e);
		} catch (IllegalAccessException e) {
			throw new FixBindingException("Cannot create bound bean",e);
		}
	}
	
	private void parseClass()
	{
		FixMessageBindingSet fbs = this.forType.getAnnotation(FixMessageBindingSet.class);
		if(fbs != null)
		{
			for(FixMessageBinding fb : fbs.fixBindings())
			{
				ClassLevelBinding clb = classLevelTags.get(fb.fixVersion());
				if(clb == null)
				{
					clb = new ClassLevelBinding();
					classLevelTags.put(fb.fixVersion(), clb);
				}
				clb.add(fb);
			}
		}
		
	}
	
	private void parseFields() throws FixBindingException 
	{
		parseFields(this.forType,new BeanPropertyHierarchy("",this.forType));
	}
	
	private void parseFields(Class<?> type,BeanPropertyHierarchy prefix) throws FixBindingException 
	{
		Field[] fields = type.getDeclaredFields();
		//at the field level this code is interested in the following
		//annotations: FixBindingSet or FixGroup
		for(Field field : fields)
		{
			FixBindingSet binding = field.getAnnotation(FixBindingSet.class);
			if(binding != null)
			{
				bindField(prefix,binding, field);
			}
			else if(field.isAnnotationPresent(FixGroupSet.class))
			{
				FixGroupSet group = field.getAnnotation(FixGroupSet.class);
				bindGroup(prefix,group,field);
			}
			else if(field.isAnnotationPresent(FixBound.class))
			{
				FixBound group = field.getAnnotation(FixBound.class);
				parseFields(group.javaType(),
						new BeanPropertyHierarchy(field.getName(),group.javaType(),prefix));
			}
		}
	}
	
	private void bindField(BeanPropertyHierarchy prefix,FixBindingSet binding, Field field) throws FixBindingException
	{
		for(FixBinding bind : binding.fixBindings())
		{
			FixBindingMetaData metadata = versionMetadata.get(bind.fixVersion());
			if(metadata == null)
			{
				metadata = new FixBindingMetaData();
				versionMetadata.put(bind.fixVersion(), metadata);
			}
			metadata.addBinding(bind,field, prefix);
		}
	}
	
	private void bindGroup(BeanPropertyHierarchy prefix,FixGroupSet binding, Field field) throws FixBindingException
	{
		for(FixGroup bind : binding.fixGroups())
		{
			FixBindingMetaData metadata = versionMetadata.get(bind.fixVersion());
			if(metadata == null)
			{
				metadata = new FixBindingMetaData();
				versionMetadata.put(bind.fixVersion(), metadata);
			}
			metadata.addGroupBinding(bind,field, prefix);
		}
	}
	
	FixBindingMetaData getMetadata(FixVersion version)
	{
		return this.versionMetadata.get(version);
	}
	
	ClassLevelBinding getClassBinding(FixVersion version)
	{
		return this.classLevelTags.get(version);
	}
	
	
	public void writeTo(MessageSink sink, Object message,FixVersion version)throws FixBindingException
	{
		FixBindingMetaData fbmd = getMetadata(version);
		if(fbmd == null)
		{
			throw new FixBindingException("Class " + message.getClass() 
					+ " is not bound to FIX version" + version);
		}
		sink.writeTag(ValidatingValidator.FIX_VERSION, EnumConverter.getEnumConverter(FixVersion.class).convertToFix(version));
		ClassLevelBinding clb = this.classLevelTags.get(version);
		if(clb != null)
		{
			clb.serialise(message, sink);
		}
		
		fbmd.serialise(message, sink);
	}
}

